Skip to content

AIFunctions

AIFunctions are single, focused operations that the agent can call. They are the simplest capability type and the building block for more complex capabilities.

Basic Usage

Mark a method with [AIFunction] to expose it to the agent:

csharp
public class CalculatorTool
{
    [AIFunction]
    public int Add(int a, int b)
    {
        return a + b;
    }
}

The agent sees this as a tool called Add that takes two integers and returns an integer.


Descriptions

Function Description

Use [AIDescription] to tell the agent what the function does:

csharp
[AIFunction]
[AIDescription("Add two numbers together and return the sum")]
public int Add(int a, int b)
{
    return a + b;
}

Parameter Descriptions

Apply [AIDescription] to parameters for clarity:

csharp
[AIFunction]
[AIDescription("Search the web for information")]
public async Task<string> WebSearch(
    [AIDescription("The search query")] string query,
    [AIDescription("Maximum number of results (1-10)")] int maxResults = 5)
{
    // Implementation
}

Return Types

AIFunctions support various return types:

csharp
// Synchronous
[AIFunction]
public string GetName() => "HPD-Agent";

// Async
[AIFunction]
public async Task<string> GetNameAsync() => await Task.FromResult("HPD-Agent");

// Complex types (serialized to JSON)
[AIFunction]
public async Task<WeatherResult> GetWeather(string city)
{
    return new WeatherResult { Temperature = 72, Condition = "Sunny" };
}

// Void (returns confirmation message)
[AIFunction]
public void LogMessage(string message)
{
    Console.WriteLine(message);
}

Requiring Permission

Some functions need user approval before execution. Use [RequiresPermission]:

csharp
[AIFunction]
[AIDescription("Delete a file from the filesystem")]
[RequiresPermission]
public void DeleteFile(string path)
{
    File.Delete(path);
}

When the agent calls this function, the user will be prompted to approve or deny the action.


Custom Function Names

Override the default method name:

csharp
[AIFunction(Name = "search_web")]
public async Task<string> PerformWebSearch(string query)
{
    // Implementation
}

The agent sees this as search_web, not PerformWebSearch.


The Collapse Attribute

When a toolkit has multiple functions, you can group them under a collapsible container using [Collapse("description")]:

csharp
[Collapse("File operations for reading, writing, and managing files")]
public class FileSystemToolkit
{
    [AIFunction]
    public string ReadFile(string path) { /* ... */ }

    [AIFunction]
    public void WriteFile(string path, string content) { /* ... */ }

    [AIFunction]
    public void DeleteFile(string path) { /* ... */ }
}

Note: Providing a description to [Collapse] enables collapsing. Without [Collapse], the toolkit's functions are always visible.

Collapse with Instructions

The [Collapse] attribute supports two instruction types that serve different purposes:

csharp
[Collapse(
    "Database operations",
    FunctionResult: "Available: Query, Insert, Update, Delete. Always use transactions for multiple operations.",
    SystemPrompt: "CRITICAL: Never execute DELETE without a WHERE clause."
)]
public class DatabaseToolkit
{
    // Functions...
}
PropertyWhere It GoesWhenPersistence
FunctionResultTool call resultOnce on expansionIn message history
SystemPromptSystem instructionsEvery turn while expandedConfigurable

FunctionResult: One-time message returned when the container expands. Use for listing available functions or one-time guidance.

SystemPrompt: Injected into system instructions while functions are expanded. Use for critical rules that must be followed every turn.

Dynamic Instructions with Expressions

Reference methods or properties for dynamic instructions:

csharp
[Collapse(
    "Search operations",
    FunctionResult: nameof(GetAvailableSearchers),
    SystemPrompt: nameof(SearchGuidelines)
)]
public class SearchToolkit
{
    public static string GetAvailableSearchers() =>
        $"Available: {string.Join(", ", _enabledSearchers)}";

    public static string SearchGuidelines =>
        "Always prefer exact matches over fuzzy matches.";

    // Functions...
}

→ See 02.1.5 Context Engineering.md for advanced collapsing configuration.


Dependency Injection

Tools can receive services through constructor injection:

csharp
public class WeatherTool
{
    private readonly IWeatherService _weatherService;
    private readonly ILogger<WeatherTool> _logger;

    public WeatherTool(IWeatherService weatherService, ILogger<WeatherTool> logger)
    {
        _weatherService = weatherService;
        _logger = logger;
    }

    [AIFunction]
    [AIDescription("Get current weather for a city")]
    public async Task<WeatherResult> GetWeather(string city)
    {
        _logger.LogInformation("Getting weather for {City}", city);
        return await _weatherService.GetCurrentWeatherAsync(city);
    }
}

Register the service provider with the agent:

csharp
var services = new ServiceCollection()
    .AddSingleton<IWeatherService, WeatherService>()
    .AddLogging()
    .BuildServiceProvider();

var agent = await new AgentBuilder()
    .WithServiceProvider(services)
    .WithToolkit<WeatherTool>()
    .BuildAsync();

Generic Attribute Form: [AIFunction<TMetadata>]

The plain [AIFunction] attribute is sufficient for most functions. Use the generic form [AIFunction<TMetadata>] when you need compile-time validation of dynamic descriptions or conditional expressions against a specific metadata type.

csharp
// Plain form — no metadata type checking
[AIFunction]
[AIDescription("Search the web")]
public async Task<string> Search(string query) { ... }

// Generic form — descriptions and conditionals are validated against SearchMetadata at compile time
[AIFunction<SearchMetadata>]
[AIDescription("Search using {metadata.ProviderName}")]
public async Task<string> Search(string query) { ... }

[AIFunction<TMetadata>] is a separate attribute class with these properties:

csharp
public sealed class AIFunctionAttribute<TMetadata> : Attribute
    where TMetadata : IToolMetadata
{
    public string? Name { get; set; }    // Custom name (uses method name if null)
    public ToolKind Kind { get; set; }   // Function or Output (default: Function)
}

The TMetadata type parameter tells the source generator which metadata class to validate expressions against. Dynamic description tokens like {metadata.ProviderName} and conditional expressions like "HasAdvancedFeatures" must resolve to valid members of TMetadata.

→ See 02.1.4 Tool Metadata.md for details on metadata and dynamic descriptions.


ToolKind — Structured Output

ToolKind controls how the framework treats the function when the agent calls it:

csharp
public enum ToolKind
{
    Function = 0,  // Normal tool — executed, result returned to the LLM
    Output   = 1   // Output tool — calling this ends the agent run; arguments ARE the result
}

ToolKind.Output for structured responses

Use ToolKind.Output when you want the agent to produce a typed, validated response instead of free-form text. The agent "calls" the output tool with its arguments — the framework captures those arguments as the structured result and never executes the method. The agent run terminates at that point.

csharp
public record WeatherReport(string City, double TempCelsius, string Condition);

public class WeatherToolkit
{
    [AIFunction(Kind = ToolKind.Output)]
    public WeatherReport Report(
        [AIDescription("City name")] string city,
        [AIDescription("Temperature in Celsius")] double tempCelsius,
        [AIDescription("Weather condition (e.g., 'sunny', 'cloudy')")] string condition)
    {
        // This method body is never executed.
        // The agent's arguments become the structured result.
        throw new NotImplementedException();
    }
}

Retrieve the result with RunStructuredAsync<T>():

csharp
var result = await agent.RunStructuredAsync<WeatherReport>(messages, branch);
// result is a WeatherReport with values the agent filled in

ToolKind.Output is compatible with both [AIFunction] and [AIFunction<TMetadata>].


Generic Attribute Forms for All Tool Types

The pattern applies to all three tool attribute types. Each has a plain form and a generic form:

PlainGenericWhen to use the generic form
[AIFunction][AIFunction<TMetadata>]Dynamic descriptions or conditionals validated at compile time
[Skill][Skill<TMetadata>]Skill visibility conditionals tied to a specific metadata type
[SubAgent][SubAgent<TMetadata>]SubAgent visibility conditionals tied to a specific metadata type

All three generic attributes follow the same rules:

  • TMetadata must implement IToolMetadata
  • Expressions in [ConditionalFunction(...)] are validated against TMetadata members
  • Dynamic description tokens ({metadata.X}) are validated against TMetadata members
csharp
public class MyToolMetadata : IToolMetadata
{
    public bool FeatureEnabled { get; set; }
    public string ProviderName { get; set; } = "Default";
    // ...
}

public class MyToolkit
{
    // Generic AIFunction — conditional validated against MyToolMetadata
    [AIFunction<MyToolMetadata>]
    [ConditionalFunction("FeatureEnabled")]
    [AIDescription("Only visible when FeatureEnabled is true")]
    public string FeatureFunction() => "...";

    // Generic Skill — skill visibility depends on metadata
    [Skill<MyToolMetadata>]
    [ConditionalFunction("FeatureEnabled")]
    public Skill FeatureWorkflow() => SkillFactory.Create(...);

    // Generic SubAgent — sub-agent visibility depends on metadata
    [SubAgent<MyToolMetadata>]
    [ConditionalFunction("FeatureEnabled")]
    public SubAgent FeatureAgent() => SubAgentFactory.Create(...);
}

→ See 02.1.4 Tool Metadata.md for how to define metadata types and register them with the agent.


Conditional Functions

Show or hide functions based on runtime conditions:

csharp
[AIFunction]
[ConditionalFunction("HasAdvancedFeatures")]
[AIDescription("Advanced search with filters")]
public async Task<string> AdvancedSearch(string query, SearchFilters filters)
{
    // Only visible when metadata.HasAdvancedFeatures is true
}

→ See 02.1.4 Tool Metadata.md for conditional registration details.


Best Practices

  1. Keep functions focused: One function should do one thing well.

  2. Write clear descriptions: The agent relies on descriptions to choose the right function.

  3. Use meaningful parameter names: city is better than c or input.

  4. Handle errors gracefully: Return error information rather than throwing exceptions when possible.

  5. Use [RequiresPermission] for destructive or sensitive operations.

  6. Group related functions: Use [Collapse("description")] to reduce context clutter.

csharp
// Good: Focused, well-described, safe
[AIFunction]
[AIDescription("Search for files matching a pattern in a directory")]
public async Task<string[]> FindFiles(
    [AIDescription("Directory to search in")] string directory,
    [AIDescription("Glob pattern (e.g., '*.txt')")] string pattern)
{
    if (!Directory.Exists(directory))
        return Array.Empty<string>();

    return Directory.GetFiles(directory, pattern);
}

// Bad: Vague, no descriptions, unsafe
[AIFunction]
public void Process(string x)
{
    File.Delete(x);  // Destructive without permission!
}

Next Steps

Released under the MIT License.